using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.XLANGs.BaseTypes;
using XmlUnit;
using System.Globalization;
using System.Xml;

namespace MapTestFramework.Common
{
    /// <summary>
    /// Represents a helper class for testing BizTalk Server maps.
    /// </summary>
    public class TestHelper
    {
        private class TestMapContext
        {
            public TestMapContext(string name, string sourcePathBase, string expectedPathBase, MapTestCases collection)
            {
                m_Name = name;
                m_SourcePathBase = sourcePathBase;
                m_ExpectedPathBase = expectedPathBase;
                m_Collection = collection;
                return;
            }

            private MapTestCases m_Collection;
            private int m_CurrentTestCaseIndex = -1;
            private string m_ExpectedPathBase;
            private string m_Name;
            private string m_SourcePathBase;

            public string ActualPath
            {
                get
                {
                    return TestHelper.GetActualPath(
                        this.ExpectedPathBase, this.Name
                        );
                }
            }
            public int CurrentTestCaseIndex
            {
                get { return m_CurrentTestCaseIndex; }
                private set { m_CurrentTestCaseIndex = value; }
            }
            public string ExpectedPath
            {
                get
                {
                    return TestHelper.GetExpectedPath(
                        this.ExpectedPathBase, this.Name
                        );
                }
            }
            public string ExpectedPathBase
            {
                get { return m_ExpectedPathBase; }
            }
            public string[] ExpectedValues
            {
                get { return m_Collection.ExpectedTestCaseValues[CurrentTestCaseIndex]; }
            }
            public string[] ExpectedXPaths
            {
                get { return m_Collection.ExpectedTestCaseValues.GetXPaths(); }
            }
            public string Name
            {
                get 
                {
                    if (this.TestCaseCount > 1)
                        return m_Name + (CurrentTestCaseIndex + 1);
                    else
                        return m_Name;
                }
            }
            public string SourcePath
            {
                get
                {
                    return TestHelper.GetSourcePath(
                        this.SourcePathBase, this.Name
                        );
                }
            }
            public string SourcePathBase
            {
                get { return m_SourcePathBase; }
            }
            public string[] SourceValues
            {
                get { return m_Collection.SourceTestCaseValues[CurrentTestCaseIndex]; }
            }
            public string[] SourceXPaths
            {
                get { return m_Collection.SourceTestCaseValues.GetXPaths(); }
            }
            public int TestCaseCount
            {
                get { return m_Collection.SourceTestCaseValues.Count; }
            }

            public bool MoveNext()
            {
                if (++this.CurrentTestCaseIndex == this.TestCaseCount)
                    return false;
                else
                    return true;
            }
        }

        #region Constructor And Intializing Members

        /// <summary>
        /// Initializes a new instance of the TestHelper class with the specified 
        /// executer of the map, template source path and template expected path.
        /// </summary>
        /// <param name="mapExecuter">MapExecuter instance.</param>
        /// <param name="sourcePathBase">Path to the source template file.</param>
        /// <param name="expectedPathBase">Path to the expected template file.</param>
        public TestHelper(IMapExecuter mapExecuter, string sourcePathBase, string expectedPathBase)
        {
            m_MapExecuter = mapExecuter;
            m_SourcePathBase = sourcePathBase;
            m_ExpectedPathBase = expectedPathBase;
            return;
        }

        #endregion

        #region Public Members

        /// <summary>
        /// Gets a path containing the original filename concatenated with the name of the test and '_Actual'.
        /// </summary>
        /// <param name="path">The original path to the file.</param>
        /// <param name="name">The name of the test.</param>
        /// <returns>The path containing the original filename concatenated with the name of the test and '_Actual'.</returns>
        public static string GetActualPath(string path, string name)
        {
            ExceptionHelper.CheckEmptyOrNullValue(path, "path");
            ExceptionHelper.CheckEmptyOrNullValue(name, "name");

            return Path.Combine(Path.GetDirectoryName(path),
                Path.GetFileNameWithoutExtension(path) + name + "_Actual" + Path.GetExtension(path));
        }
        /// <summary>
        /// Gets a path containing the original filename concatenated with the name of the test and '_Expected'.
        /// </summary>
        /// <param name="path">The original path to the file.</param>
        /// <param name="name">The name of the test.</param>
        /// <returns>The path containing the original filename concatenated with the name of the test and '_Expected'.</returns>
        public static string GetExpectedPath(string path, string name)
        {
            ExceptionHelper.CheckEmptyOrNullValue(path, "path");
            ExceptionHelper.CheckEmptyOrNullValue(name, "name");

            return Path.Combine(Path.GetDirectoryName(path),
                Path.GetFileNameWithoutExtension(path) + name + "_Expected" + Path.GetExtension(path));
        }
        /// <summary>
        /// Gets a path containing the original filename concatenated with the name of the test.
        /// </summary>
        /// <param name="path">The original path to the file.</param>
        /// <param name="name">The name of the test.</param>
        /// <returns>The path containing the original filename concatenated with the name of the test.</returns>
        public static string GetSourcePath(string path, string name)
        {
            ExceptionHelper.CheckEmptyOrNullValue(path, "path");
            ExceptionHelper.CheckEmptyOrNullValue(name, "name");

            return Path.Combine(Path.GetDirectoryName(path),
                Path.GetFileNameWithoutExtension(path) + name + Path.GetExtension(path));
        }
        /// <summary>
        /// Gets the name of the first method decorated with the TestMethod attribute, which invoked this method directly or indirectly.
        /// </summary>
        /// <returns>The name of the test method.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public static string GetTestMethodName()
        {
            StackTrace stackTrace = new StackTrace();
            MethodBase stackFrameMethod;
            int frameCount = 0;
            do
            {
                StackFrame stackFrame = stackTrace.GetFrame(frameCount++);
                if (stackFrame == null)
                    throw new InvalidOperationException("Method not called from a test method.");

                stackFrameMethod = stackFrame.GetMethod();
            } while (stackFrameMethod.GetCustomAttributes(typeof(Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute), true).Length == 0);
            return stackFrameMethod.Name;
        }

        /// <summary>
        /// Occurs just after the test has been executed and before assertion.
        /// </summary>
        public event EventHandler<TestEventArgs> TestExecuted;
        /// <summary>
        /// Occurs just before the test is being executed.
        /// </summary>
        public event EventHandler<TestEventArgs> TestExecuting;

        private string m_ExpectedPathBase;
        private IMapExecuter m_MapExecuter;
        private string m_SourcePathBase;

        /// <summary>
        /// Gets the path of the file that acts as the template for the expected files.
        /// </summary>
        public string ExpectedPathBase
        {
            get { return m_ExpectedPathBase; }
            set { m_ExpectedPathBase = value; }
        }
        /// <summary>
        /// Get the map used in the tests.
        /// </summary>
        public IMapExecuter MapExecuter
        {
            get { return m_MapExecuter; }
        }
        /// <summary>
        /// Gets the path of the file that acts as the template for the source files.
        /// </summary>
        public string SourcePathBase
        {
            get { return m_SourcePathBase; }
        }

        /// <summary>
        /// Tests whether the map correctly maps the source message to the destination message.
        /// </summary>
        /// <param name="name">Name of the test.</param>
        public void TestMap(string name)
        {
            CreateEditableWorkingCopy(
                this.SourcePathBase, GetSourcePath(this.SourcePathBase, name)
                );
            CreateEditableWorkingCopy(
                this.ExpectedPathBase, GetExpectedPath(this.ExpectedPathBase, name)
                );
            if (File.Exists(GetActualPath(this.ExpectedPathBase, name)))
                File.SetAttributes(GetActualPath(this.ExpectedPathBase, name), FileAttributes.Archive);
            this.TestMap(
                name, GetSourcePath(this.SourcePathBase, name),
                GetActualPath(this.ExpectedPathBase, name), GetExpectedPath(this.ExpectedPathBase, name)
                );
            return;
        }
        /// <summary>
        /// Tests whether the map correctly maps the source message to the destination message for each specified testcase.
        /// </summary>
        /// <param name="name">Name of the test.</param>
        /// <param name="collection">Collection containing the testcases to perform.</param>
        public void TestMap(string name, MapTestCases collection)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            if (collection == null)
                throw new ArgumentNullException("collection");

            ExceptionHelper.CheckMapTestCaseCollection(collection, "collection");

            TestMapContext context = new TestMapContext(name, this.SourcePathBase, this.ExpectedPathBase, collection);
            this.TestMap(context);
            return;
        }
        private void TestMap(TestMapContext context)
        {
            while(context.MoveNext())
            {
                XmlHelper.UpdateNodes(context.SourceXPaths, context.SourceValues,
                    context.SourcePathBase, context.SourcePath
                    );
                XmlHelper.UpdateNodes(context.ExpectedXPaths, context.ExpectedValues,
                    context.ExpectedPathBase, context.ExpectedPath
                    );
                this.TestMap(context.Name, context.SourcePath,
                    context.ActualPath, context.ExpectedPath
                    );
            }
            return;
        }

        #endregion

        #region Event Members

        protected virtual void OnTestExecuted(TestEventArgs testEventArgs)
        {
            if (TestExecuted != null)
                TestExecuted(this, testEventArgs);
            return;
        }
        protected virtual void OnTestExecuting(TestEventArgs testEventArgs)
        {
            if (TestExecuting != null)
                TestExecuting(this, testEventArgs);
            return;
        }

        #endregion

        #region Helper Members

        private static void AssertXmlFiles(string actualPath, string expectedPath)
        {
            using (StreamReader actualReader = new StreamReader(actualPath))
            using (StreamReader expectedReader = new StreamReader(expectedPath))
                XmlAssertion.AssertXmlEquals(expectedReader, actualReader);
        }
        private static void CreateEditableWorkingCopy(string sourcePath, string destinationPath)
        {
            if (File.Exists(destinationPath))
                File.SetAttributes(destinationPath, FileAttributes.Archive);

            File.Copy(sourcePath, destinationPath, true);
            File.SetAttributes(destinationPath, FileAttributes.Archive);
            return;
        }
        private static XmlReader CreateFormattingReader(string sourcePath)
        {
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.IgnoreWhitespace = true;
            return XmlReader.Create(sourcePath, settings);
        }
        private static XmlWriter CreateFormattingWriter(string destPath)
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            return XmlWriter.Create(destPath, settings);
        }
        private static void FormatXml(string sourcePath)
        {
            string tempPath = Path.GetTempFileName();
            using (XmlReader reader = CreateFormattingReader(sourcePath))
            {
                using (XmlWriter writer = CreateFormattingWriter(tempPath))
                {
                    while (reader.Read())
                    {
                        writer.WriteNode(reader, false);
                    }
                    writer.Flush();
                    writer.Close();
                }
            }
            File.Copy(tempPath, sourcePath, true);
            return;
        }
        private static void TraceTestCompleted(string testName)
        {
            Trace.WriteLine(Properties.Resources.TraceMessageSeparator);
            Trace.WriteLine(
                string.Format(CultureInfo.InvariantCulture,
                    Properties.Resources.TraceMessageCompletedTest, testName
                    )
                );
            Trace.WriteLine(Properties.Resources.TraceMessageSeparator);
        }
        private static void TraceTestStart(string testName, string sourcePath, string actualPath, string expectedPath)
        {
            Trace.WriteLine(Properties.Resources.TraceMessageSeparator);
            Trace.WriteLine(
                string.Format(CultureInfo.InvariantCulture,
                    Properties.Resources.TraceMessageStartingTest, testName
                    )
                );
            Trace.WriteLine(Properties.Resources.TraceMessageSeparator);
            Trace.WriteLine(
                string.Format(CultureInfo.InvariantCulture,
                    Properties.Resources.TraceMessageSourcePath,
                    Path.GetFullPath(sourcePath)
                    )
                );
            Trace.WriteLine(
                string.Format(CultureInfo.InvariantCulture,
                    Properties.Resources.TraceMessageActualPath,
                    Path.GetFullPath(actualPath)
                    )
                );
            Trace.WriteLine(
                string.Format(CultureInfo.InvariantCulture,
                    Properties.Resources.TraceMessageExpectedPath,
                    Path.GetFullPath(expectedPath)
                    )
                );
        }

        private void TestMap(string testName, string sourcePath, string actualPath, string expectedPath)
        {
            TraceTestStart(testName, sourcePath, actualPath, expectedPath);
            TestEventArgs e = new TestEventArgs(testName, sourcePath, actualPath, expectedPath);
            OnTestExecuting(e);
            // Format source file to prevent whitespace in empty elements from 
            // creating newline characters in the target file.
            FormatXml(sourcePath);
            this.MapExecuter.ExecuteMap(sourcePath, actualPath);
            OnTestExecuted(e);
            // Format actual and expected files to show empty elements in a 
            // consistent <...></...> form instead of using two lines.
            FormatXml(actualPath);
            FormatXml(expectedPath);
            TraceTestCompleted(testName);
            AssertXmlFiles(actualPath, expectedPath);
            return;
        }

        #endregion
    }
}
